﻿using System;
using System.Linq;
using System.Diagnostics;
using System.IO;
using System.Collections;
using System.Collections.Generic;
using System.Data;
using System.Xml;
using System.Xml.XPath;
using System.Xml.Xsl;
using System.Text;
using System.Text.RegularExpressions;
using System.Web;
using System.Reflection;

namespace Landscape
{
    /// <summary>
    /// The main class of Landscape, takes care of XSL rendering operations
    /// </summary>
    public class Renderer {

        #region Configurable properties

        /// <summary>
        /// Temporary setting which change at every global rendering operation
        /// </summary>
        public static Dictionary<string, string> SessionSettings = new Dictionary<string, string>();
        #endregion

        #region Internal properties


        /// <summary>
        /// The System.Web.UI.WebControls.PlaceHolder which will be populated with the render result
        /// </summary>
        internal static System.Web.UI.WebControls.PlaceHolder placeholder;
        internal static Dictionary<string, Elements.Control> PageControls = new Dictionary<string, Elements.Control>();

        /// <summary>
        /// Core extensions along with all extensions defined into configuration
        /// </summary>
        /// <returns></returns>
        internal static Dictionary<string, object> _XSLTExtensions;
        internal static Dictionary<string, object> XSLTExtensions {
            get {
                if (_XSLTExtensions == null) {
                    _XSLTExtensions = new Dictionary<string, object>();

                    if (Context.Instance.xsltExtensions != null && Context.Instance.xsltExtensions.Count > 0) {
                        foreach (string alias in Context.Instance.xsltExtensions.Keys) {
                            Members.ExtensionObject extensionObject = Members.ExtensionObject.GetExtensionObject(alias, Context.Instance.xsltExtensions[alias]);

                            _XSLTExtensions.Add(extensionObject.Alias, extensionObject.Extension);
                        }
                    }
                }

                return _XSLTExtensions;
            }
        }

        #endregion

        private Renderer()
        {
        }

        #region Public render methods
        /// <summary>
        /// Render the template identified by templateName using the specific System.Xml.XmlDocument instance
        /// </summary>
        /// <param name="templateName">The template name, without extension</param>
        /// <param name="source">An object which can be converted into a valid XPathDocument</param>
        /// <returns>The rendered content</returns>
        public static string Render(string templateName, object source) {
            return Renderer.Render(templateName, source, null);
        }
        /// <summary>
        /// Render the template identified by templateName using the specific System.Xml.XmlDocument instance
        /// </summary>
        /// <param name="templateName">The template name, without extension</param>
        /// <param name="source">An object which can be converted into a valid XPathDocument</param>
        /// <param name="templateParams">A dictionary of optional params to supply to the template</param>
        /// <returns>The rendered content</returns>
        public static string Render(string templateName, object source, IDictionary<string, object> templateParams) {
            XPathDocument xdoc;
            LoadXPathDocument(source, out xdoc);

            string result = Render(templateName, xdoc, templateParams);

            // reset rendering session settings
            // this has to be done only on public Render methods in order for it to be executed only once
            SessionSettings = new Dictionary<string, string>();

            return result;
        }

        /// <summary>
        /// Render the template identified by templateName using the specific System.Data.DataSet instance
        /// </summary>
        /// <param name="templateName">The template name, without extension</param>
        /// <param name="source">A valid System.Data.DataSet instance</param>
        /// <param name="contentPlaceholder">The System.Web.UI.WebControls.PlaceHolder which will be populated with the render result</param>
        /// <returns>The rendered content</returns>
        public static void RenderToPlaceHolder(string templateName, object source, System.Web.UI.WebControls.PlaceHolder contentPlaceholder) {
            Renderer.RenderToPlaceHolder(templateName, source, contentPlaceholder, null);
        }

        /// <summary>
        /// Render the template identified by templateName using the specific System.Data.DataSet instance
        /// </summary>
        /// <param name="templateName">The template name, without extension</param>
        /// <param name="source">A valid System.Data.DataSet instance</param>
        /// <param name="contentPlaceholder">The System.Web.UI.WebControls.PlaceHolder which will be populated with the render result</param>
        /// <param name="templateParams">The parameters to supply to the selected template</param> 
        /// <returns>The rendered content</returns>
        public static void RenderToPlaceHolder(string templateName, object source, System.Web.UI.WebControls.PlaceHolder contentPlaceholder, IDictionary<string, object> templateParams)
        {
            placeholder = contentPlaceholder;

            XPathDocument xdoc;
            LoadXPathDocument(source, out xdoc);

            string result = Render(templateName, xdoc, templateParams);

            result = result.Replace("xmlns:asp=\"remove\"", "");


            placeholder.Controls.Add(placeholder.Page.ParseControl(result));
            System.Web.UI.Control parsedControl = placeholder.Controls[0];

            // process user controls
            if (PageControls.Count > 0)
            {
                foreach (Elements.Control control in PageControls.Values)
                {
                    System.Web.UI.WebControls.PlaceHolder ucPlaceholder = (System.Web.UI.WebControls.PlaceHolder)Globals.FindControlRecursive(parsedControl, control.PlaceHolderId);

                    if (placeholder.FindControl(control.PlaceHolderId) != null) {
                        System.Web.UI.Control uc = Renderer.placeholder.Page.TemplateControl.LoadControl(control.VirtualPath);
                        uc.ID = control.ControlId;

                        if (control.Properties.Count > 0)
                        {
                            foreach (KeyValuePair<string, object> param in control.Params) {
                                // parameter has a corresponding set property
                                if (control.Properties[param.Key] != null && param.Value != null)
                                {
                                    string propertyType = control.Properties[param.Key];
                                    object propertyValue = null;

                                    switch (propertyType) { 
                                        case "System.Int32":
                                            propertyValue = Convert.ToInt32(param.Value);
                                            break;
                                        case "System.String":
                                            propertyValue = param.Value;
                                            break;
                                        case "System.Boolean":
                                            int propertyTempValue = Convert.ToInt32(param.Value);
                                            propertyValue = propertyTempValue == 1 ? true : false;
                                            break;
                                    }

                                    // dynamically set object property
                                    if (propertyValue != null) {
                                        uc.GetType().GetProperty(param.Key).SetValue(uc, propertyValue, null);                                    
                                    }
                                }
                            }
                        }

                        ucPlaceholder.Controls.Add(uc);
                    }
                }
            }            

            // reset rendering session settings
            // this has to be done only on public Render methods in order for it to be executed only once
            SessionSettings = new Dictionary<string, string>();
        }
        /// <summary>
        /// Renders a widget using the specified parameters
        /// </summary>
        /// <param name="name">The name of the widgets</param>
        /// <param name="source">An object which can be converted into an XPathDocument</param>
        /// <param name="content">The widget content.</param>
        /// <param name="parameters">A dictionary of string parameters supplied to the widget</param>
        /// <returns>The rendered string</returns>
        public static string RenderWidget(string name, object source, string content, Dictionary<string, object> parameters) {
            XPathDocument xdoc;
            LoadXPathDocument(source, out xdoc);

            Elements.Widget w = new Elements.Widget() { Content = content, Params = parameters, Name = name };

            string widgetFile = w.Name + Context.xslExtension;
            string widgetPath = Globals.LookupFile(widgetFile, Context.Instance.widgetPaths);

            if (string.IsNullOrEmpty(widgetPath)) {
                throw new Exception("Widget not found after lookup in " + Context.Instance.widgetPaths.Count + " dirs: " + widgetFile);
            }

            WidgetTemplate template = Template.GetTemplate<WidgetTemplate>(widgetPath);

            byte[] result = null;
            using (Stream ms = Context.Instance.compiler.Transform(xdoc, template.TemplateCompiledTransform, w.Params)) {


                XmlReaderSettings inSettings = new XmlReaderSettings() {
                    DtdProcessing = DtdProcessing.Parse,
                    ConformanceLevel = ConformanceLevel.Fragment,
                    XmlResolver = null
                };

                XmlWriterSettings outSettings = new XmlWriterSettings() {
                    ConformanceLevel = ConformanceLevel.Fragment,
                    OmitXmlDeclaration = true,
                    Indent = template.OutputSettings.Indent
                };

                MemoryStream res = new MemoryStream();
                XmlReader reader = XmlReader.Create(ms, inSettings);
                XmlWriter writer = XmlWriter.Create(res, outSettings);
                while (reader.Read()) {

                    switch (reader.NodeType) {
                        case XmlNodeType.CDATA:
                            writer.WriteCData(reader.Value);
                            break;
                        case XmlNodeType.Comment:
                            writer.WriteComment(reader.Value);
                            break;
                        case XmlNodeType.DocumentType:
                            writer.WriteDocType(reader.Name, reader.GetAttribute("PUBLIC"), reader.GetAttribute("SYSTEM"), reader.Value);
                            break;
                        case XmlNodeType.Element:
                            switch (reader.Name) {
                                case "lsc:widget":
                                    Elements.Widget wChild = new Elements.Widget(reader);

                                    string wResult = Renderer.RenderWidget(wChild.Name, xdoc, wChild.Content, wChild.Params);
                                    writer.WriteRaw(wResult);
                                    break;
                                default:
                                    writer.WriteStartElement(reader.Prefix, reader.LocalName, reader.NamespaceURI);
                                    writer.WriteAttributes(reader, true);
                                    if (reader.IsEmptyElement) {
                                        writer.WriteEndElement();
                                    }
                                    break;
                            }
                            break;
                        case XmlNodeType.EndElement:
                            writer.WriteFullEndElement();
                            break;
                        case XmlNodeType.EntityReference:
                            writer.WriteEntityRef(reader.Name);
                            break;
                        case XmlNodeType.Text:
                            writer.WriteString(reader.Value);
                            break;
                        case XmlNodeType.Whitespace:
                        case XmlNodeType.SignificantWhitespace:
                            writer.WriteWhitespace(reader.Value);
                            break;
                        case XmlNodeType.XmlDeclaration:
                        case XmlNodeType.ProcessingInstruction:
                            writer.WriteProcessingInstruction(reader.Name, reader.Value);
                            break;
                    }
                }
                writer.Flush();
                writer.Close();
                reader.Close();
                res.Position = 0;
                result = res.ToArray();
                res.Close();
                ms.Close();
            }

            return System.Text.Encoding.UTF8.GetString(result);
        }

        #endregion

        #region Actual render methods
        /// <summary>
        /// Renders the specified XSLT template using a valid XPath Document
        /// </summary>
        /// <param name="templatePath">The physical path to the XSLT template</param>
        /// <param name="xdoc">The Source Document</param>
        /// <param name="placeholderContent">An optional content for the wrapper</param>
        /// <param name="templateParams">A dictionary of params supplied to the template</param>
        /// <param name="wrapperName">The name of the wrapper of this page, if any</param>
        /// <param name="wrapperParams">A dictionary of params supplied to the wrapper, if any</param>
        /// <returns>The rendered content</returns>
        private static string RenderTemplate(string templatePath, 
                                             XPathDocument xdoc, 
                                             string placeholderContent, 
                                             IDictionary<string, object> templateParams, 
                                             out string wrapperName, 
                                             out IDictionary<string, object> wrapperParams) {
            wrapperName = null;
            wrapperParams = null;

            DocumentTemplate template = Template.GetTemplate<DocumentTemplate>(templatePath);

            ///START CAZZEGGIO
            Dictionary<string, object> foundParams = new Dictionary<string, object>();
            byte[] result = null;
            using (MemoryStream ms = Context.Instance.compiler.Transform(xdoc, Template.GetTemplate<DocumentTemplate>(templatePath).TemplateCompiledTransform, templateParams)) {
                
                
                XmlReaderSettings inSettings = new XmlReaderSettings() {
                    DtdProcessing = DtdProcessing.Parse,
                    ConformanceLevel = template.IsMaster ? ConformanceLevel.Document : ConformanceLevel.Fragment,
                    XmlResolver = null
                };

                XmlWriterSettings outSettings = new XmlWriterSettings() {
                    ConformanceLevel = template.IsMaster ? ConformanceLevel.Document : ConformanceLevel.Fragment,
                    OmitXmlDeclaration = template.IsMaster ? template.OutputSettings.OmitXmlDeclaration : true,
                    Indent = template.OutputSettings.Indent
                };

                MemoryStream res = new MemoryStream();
                XmlReader reader = XmlReader.Create(ms, inSettings);
                XmlWriter writer = XmlWriter.Create(res, outSettings);
                    while (reader.Read()) {

                        switch (reader.NodeType) {
                            case XmlNodeType.CDATA:
                                writer.WriteCData(reader.Value);
                                break;
                            case XmlNodeType.Comment:
                                writer.WriteComment(reader.Value);
                                break;
                            case XmlNodeType.DocumentType:
                                writer.WriteDocType(reader.Name, reader.GetAttribute("PUBLIC"), reader.GetAttribute("SYSTEM"), reader.Value);
                                break;
                            case XmlNodeType.Element:
                                switch(reader.Name){
                                    case "lsc:pholder":
                                        writer.WriteRaw(!string.IsNullOrEmpty(placeholderContent) ? placeholderContent : "");
                                        break;
                                    case "lsc:widget":
                                        Elements.Widget w = new Elements.Widget(reader);

                                        string wResult = Renderer.RenderWidget(w.Name, xdoc, w.Content, w.Params);
                                        writer.WriteRaw(wResult);
                                        break;
                                    case "lsc:control":
                                        Elements.Control c  = new Elements.Control(reader);
                                        if (!Renderer.PageControls.ContainsKey(c.ControlId)) {
                                            Renderer.PageControls.Add(c.ControlId, c);                                        
                                        }

                                        writer.WriteStartElement("asp", "PlaceHolder", "remove");
                                        writer.WriteAttributeString("id", c.PlaceHolderId);
                                        writer.WriteAttributeString("runat", "server");
                                        writer.WriteEndElement();
                                        break;
                                    case "lsc:param":
                                        Elements.Parameter p = new Elements.Parameter(reader);
                                        foundParams.Add(p.Name, p.Value);
                                        break;
                                    default:
                                        writer.WriteStartElement(reader.Prefix, reader.LocalName, reader.NamespaceURI);
                                        writer.WriteAttributes(reader, true);
                                        if (reader.IsEmptyElement) {
                                            writer.WriteEndElement();
                                        }
                                        break;
                                }
                                break;
                            case XmlNodeType.EndElement:
                                writer.WriteFullEndElement();
                                break;
                            case XmlNodeType.EntityReference:
                                writer.WriteEntityRef(reader.Name);
                                break;
                            case XmlNodeType.Text:
                                writer.WriteString(reader.Value);
                                break;
                            case XmlNodeType.Whitespace:
                            case XmlNodeType.SignificantWhitespace:
                                writer.WriteWhitespace(reader.Value);
                                break;
                            case XmlNodeType.XmlDeclaration:
                            case XmlNodeType.ProcessingInstruction:
                                writer.WriteProcessingInstruction(reader.Name, reader.Value);
                                break;
                        }
                    }
                writer.Flush();
                writer.Close();
                reader.Close();
                res.Position = 0;
                result = res.ToArray();     
                ms.Close();
            }

            string resultString = System.Text.Encoding.UTF8.GetString(result.ToArray());
            /////END CAZZEGGIO

            // performs master transformation and return result
            if (!template.IsMaster) {
                wrapperName = template.WrapperName;
                wrapperParams = foundParams;
            } else {
                ///TODO: need to get saxon compiler wrapper up and running soon, this shit is awful
                if (template.OutputMethod == OutputMethods.xhtml) {
                    resultString = Regex.Replace(resultString, "<(?<tag>div|span|p|script|style|title|select|option|a|ul|li)(?<attrs> [^>]*)?/>", "<${tag}${attrs}></${tag}>");
                }
            }

            return resultString;
        }

        /// <summary>
        /// Starts a rendering operation
        /// </summary>
        /// <param name="templateName">The template where to start render</param>
        /// <param name="xmldom">The source document</param>
        /// <param name="templateParams">A dictionary of parameters supplied to the template</param>
        /// <returns>The rendered content</returns>
        private static string Render(string templateName, XPathDocument xmldom, IDictionary<string, object> templateParams) {
            string procTemplate = templateName;
            IDictionary<string, object> procParams = templateParams;
            int procOrder = 0;
            string buffer = null;

            Landscape.Diagnostics.RenderSession diag = new Landscape.Diagnostics.RenderSession();
            DateTime timerOn = DateTime.Now;
            while (!string.IsNullOrEmpty(procTemplate)) {
                string templateFile = procTemplate + Context.xslExtension;
                string templatePath = Globals.LookupFile(templateFile, Context.Instance.templatePaths);
                DateTime timerTaskOn;
                TimeSpan taskElapsed;

                if (!System.IO.File.Exists(templatePath)) {
                    throw new TemplateNotFoundException(templatePath, Context.Instance.templatePaths.Values.ToArray(), String.Format("Cannot find template {0} after lookup in {1} directories", templateName, Context.Instance.templatePaths.Count.ToString()));
                }

                ///FIXME: this sucks a bit of memory
                try {
                    timerTaskOn = DateTime.Now;
                    buffer = RenderTemplate(templatePath, xmldom, buffer, procParams, out procTemplate, out procParams);
                    taskElapsed = (DateTime.Now - timerTaskOn);
                    diag.Tasks.Add(new Diagnostics.RenderTask() { Duration = taskElapsed, TemplatePath = templatePath, Order = procOrder });
                    procOrder++;
                } catch (Exception ex) {
                    throw new RenderException(templatePath, String.Format("An error occurred while rendering template: {0} at {1}", ex.Message, templatePath), ex);
                }
            }
            diag.Duration = DateTime.Now - timerOn;
            if (HttpContext.Current != null && HttpContext.Current.Session != null) {
                HttpContext.Current.Session.Add("rendererlastsession", diag);            
            }

            return buffer;
        }
        #endregion

        private static XmlNode CreateDocumentFragment(XmlDocument doc, string content)
        {
            XmlDocumentFragment fragment = doc.CreateDocumentFragment();
            fragment.InnerXml = content;
            return fragment;
        }

        public static void LoadXPathDocument(object source, out XPathDocument xdoc) {
            if (source == null) {
                throw new ArgumentNullException("source");
            }else if (source is XPathDocument) {
                xdoc = (XPathDocument)source;
            }else if (source is XmlDocument) {
                XmlDocument doc = (XmlDocument)source;
                xdoc = new XPathDocument(new StringReader(doc.OuterXml));
            } else if (source is DataSet) {
                DataSet ds = (DataSet)source;
                using (MemoryStream dataStream = new MemoryStream()) {
                    ds.WriteXml(dataStream);
                    dataStream.Position = 0;
                    xdoc = new XPathDocument(dataStream);
                }
            }else if(source is System.IO.Stream){
                xdoc = new XPathDocument((System.IO.Stream)source);
            } else if (source is string) {
                xdoc = new XPathDocument(new StringReader((string)source));
            } else {
                string msg = string.Format("Source type '{0}' has no valid implementation", source.GetType().FullName);
                throw new InvalidSourceTypeException(source, msg);
            }
        }
    }
}